// Package repository
// Created by GoLand
// @User: lenora
// @Date: 2023/11/29
// @Time: 18:27

package repository

import (
	"camp/microbook/internal/domain"
	"camp/microbook/internal/repository/cache"
	"camp/microbook/internal/repository/dao"
	"context"
	"database/sql"
	"log"
	"time"
)

var (
	ErrDuplicateUser = dao.ErrDuplicateUser
	ErrUserNotFound  = dao.ErrRecordNotFound
)

type UserRepository interface {
	Create(ctx context.Context, u domain.UserBase) error
	Edit(ctx context.Context, u domain.User) error
	FindById(ctx context.Context, uid uint64) (domain.User, error)
	FindByName(ctx context.Context, name string) (domain.UserBase, error)
	FindByPhone(ctx context.Context, phone string) (domain.UserBase, error)
	FindByOpenid(ctx context.Context, openid string) (domain.UserBase, error)
}

type userRepository struct {
	dao   dao.UserDAO
	cache cache.UserCache
}

func NewUserRepository(d dao.UserDAO, c cache.UserCache) UserRepository {
	return &userRepository{
		dao:   d,
		cache: c,
	}
}

func (repo *userRepository) Create(ctx context.Context, u domain.UserBase) error {
	return repo.dao.Insert(ctx, dao.User{
		UserName: sql.NullString{String: u.UserName, Valid: u.UserName != ""},
		Phone:    sql.NullString{String: u.Phone, Valid: u.Phone != ""},
		Password: u.Password,
		Openid: sql.NullString{
			String: u.Openid,
			Valid:  u.Openid != "",
		},
	})
}

func (repo *userRepository) FindByName(ctx context.Context, name string) (domain.UserBase, error) {
	u, err := repo.dao.FindByName(ctx, name)
	if err != nil {
		return domain.UserBase{}, err
	}

	user := domain.UserBase{
		Id:       u.Id,
		UserName: u.UserName.String,
		Password: u.Password,
	}
	return user, nil
}

func (repo *userRepository) FindByPhone(ctx context.Context, phone string) (domain.UserBase, error) {
	u, err := repo.dao.FindByPhone(ctx, phone)
	if err != nil {
		return domain.UserBase{}, err
	}
	return domain.UserBase{
		Id:       u.Id,
		UserName: u.UserName.String,
		Phone:    u.Phone.String,
		Password: u.Password,
	}, err
}

func (repo *userRepository) FindById(ctx context.Context, uid uint64) (domain.User, error) {
	//引入缓存
	uCache, err := repo.cache.Get(ctx, uid)
	if err == nil {
		return uCache, nil
	}
	// 缓存查询出错即查询数据库
	// 查询失败的可能性
	// 1.key不存在: redis执行正常
	// 2.访问redis出错: 可能网络问题;也可能redis崩溃
	u, err := repo.dao.FindById(ctx, uid)
	if err != nil {
		return domain.User{}, err
	}
	ud := repo.toDomain(u)
	// 缓存异步写入
	// 进一步提高查询性能
	//go func() {
	//_ = repo.cache.Set(ctx, ud)
	if err := repo.cache.Set(ctx, ud); err != nil {
		log.Println(err)
	}
	//}()
	return ud, nil
}

// findByIdBetter
// 高并发情况下推荐使用的写法
// 避免数据库也发生崩溃
func (repo *userRepository) findByIdBetter(ctx context.Context, uid uint64) (domain.User, error) {
	//引入缓存
	uCache, err := repo.cache.Get(ctx, uid)
	switch err {
	case nil:
		return uCache, nil
	case cache.ErrKeyNotExist:
		//key 不存在
		u, err := repo.dao.FindById(ctx, uid)
		if err != nil {
			return domain.User{}, err
		}
		ud := repo.toDomain(u)
		// 缓存异步写入
		// 进一步提高查询性能
		go func() {
			_ = repo.cache.Set(ctx, ud)
		}()
		return ud, nil
	default:
		//redis 服务崩溃,直接返回空值,避免向数据库发出大量请求
		return domain.User{}, err
	}

}

func (repo *userRepository) Edit(ctx context.Context, u domain.User) error {
	return repo.dao.UpdateById(ctx, repo.toEntity(u))
}

func (repo *userRepository) FindByOpenid(ctx context.Context, openid string) (domain.UserBase, error) {
	u, err := repo.dao.FindByOpenid(ctx, openid)
	if err != nil {
		return domain.UserBase{}, err
	}
	return domain.UserBase{
		Id:       u.Id,
		UserName: u.UserName.String,
		Phone:    u.Phone.String,
		Password: u.Password,
		Openid:   u.Openid.String,
	}, nil
}

func (repo *userRepository) toDomain(u dao.User) domain.User {
	return domain.User{
		Id:       u.Id,
		UserName: u.UserName.String,
		Phone:    u.Phone.String,
		Nickname: u.Nickname,
		Birth:    u.Birth,
		Desc:     u.Desc,
		Ctime:    time.UnixMilli(u.Ctime),
	}
}

func (repo *userRepository) toEntity(u domain.User) dao.User {
	return dao.User{
		Id: u.Id,
		UserName: sql.NullString{
			String: u.UserName,
			Valid:  u.UserName != "",
		},
		Phone: sql.NullString{
			String: u.Phone,
			Valid:  u.Phone != "",
		},
		Nickname: u.Nickname,
		Birth:    u.Birth,
		Desc:     u.Desc,
	}
}
